Skip to content

Fix duplicate auth headers and cookie forwarding in proxy#23

Open
irees wants to merge 2 commits into
mainfrom
fix-proxy-duplicate-auth-headers
Open

Fix duplicate auth headers and cookie forwarding in proxy#23
irees wants to merge 2 commits into
mainfrom
fix-proxy-duplicate-auth-headers

Conversation

@irees
Copy link
Copy Markdown
Contributor

@irees irees commented May 20, 2026

Summary

Fixes authenticated proxy requests being rejected by the backend API with token is malformed: token contains an invalid number of segments (HTTP 401). Two related bugs in the server-side auth/proxy path, found while migrating www-transit-land-v2 off tlv2-ui.

Duplicate Authorization header (the 401)

auth.server overrides globalThis.$fetch and globalThis.fetch to inject Authorization/apikey on requests to configured backend origins, using Headers.append(). The server-side proxy (buildProxyHeaders) already attaches those headers, and its outbound request flows through the patched globalThis.fetch — so the headers were appended a second time, comma-joining into:

Authorization: Bearer <tok>, Bearer <tok>

The backend strips the first Bearer prefix, parses <tok>, Bearer <tok>, gets the wrong number of dot-separated segments, and rejects the token. This broke every authenticated GraphQL/REST call through the proxy and the me role-enrichment query (empty roles → role-gated UI shows "Feature unavailable").

Fix: both injection paths now use Headers.set() instead of append(). set() is idempotent — when a request already carries the header (the proxy case), it collapses to a single correct value rather than duplicating.

Browser session cookie forwarded upstream

The proxy forwarded the incoming browser headers wholesale (h3's default), including the __a0_session cookie. That cookie is irrelevant to the backend API and — being the encrypted auth0-nuxt session — effectively duplicates the JWT we already attach, leaking it to the upstream.

Fix: set headers.cookie = '' before proxyRequest. h3's mergeHeaders ignores undefined but applies empty strings, so this overrides the forwarded Cookie rather than leaving it intact.

Test plan

  • pnpm lint
  • pnpm test ✓ (35/35)
  • Verified end-to-end against the live backend through www-transit-land-v2's proxy: authenticated GraphQL requests return data (previously 401), and the wire request to the backend carries exactly one Authorization and no session cookie.

🤖 Generated with Claude Code

irees and others added 2 commits May 20, 2026 03:16
The auth.server plugin injected Authorization/apikey onto backend requests
with Headers.append(). Because the server-side proxy already attaches those
headers, the request — which flows through the patched globalThis.fetch — had
them appended a second time, comma-joining into
"Authorization: Bearer <tok>, Bearer <tok>". The backend parsed that as a
malformed JWT and rejected it with 401 ("token contains an invalid number of
segments"), breaking every authenticated GraphQL/REST call and the `me`
role-enrichment query. Switch both injection paths ($fetch onRequest and the
globalThis.fetch override) to Headers.set(), which is idempotent.

Also stop forwarding the browser session cookie to the backend API in the
proxy: it is irrelevant to the API and the encrypted auth0-nuxt session cookie
duplicates the JWT. Uses an empty-string override since h3's mergeHeaders
ignores undefined but applies empty strings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first commit only converted the $fetch onRequest loop; the two injection
loops have different indentation so the bulk replace missed the globalThis.fetch
override — which is the exact path the proxy's outbound request flows through.
Convert it too so authenticated proxy requests don't get a duplicated
Authorization header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@irees irees marked this pull request as ready for review May 20, 2026 10:47
Copilot AI review requested due to automatic review settings May 20, 2026 10:47
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an SSR/server-proxy authentication failure caused by duplicated Authorization/apikey headers being appended twice, and prevents forwarding the browser’s Auth0 session cookie upstream to the backend API.

Changes:

  • Switch server-side auth header injection in auth.server from Headers.append() to Headers.set() to avoid comma-joined duplicate auth headers.
  • Clear the cookie header in the proxy handler to prevent forwarding the browser session cookie to backend APIs.
  • Add a changeset documenting the patch-level fix.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/runtime/util/proxy.ts Clears cookie before proxying to prevent forwarding browser session cookies upstream.
src/runtime/plugins/auth.server.ts Uses Headers.set() to avoid duplicate Authorization/apikey headers in SSR/server requests.
.changeset/proxy-no-duplicate-auth.md Records the patch release notes for the proxy/auth header fixes.
Comments suppressed due to low confidence (1)

src/runtime/plugins/auth.server.ts:97

  • Same issue in the globalThis.fetch wrapper: headers.set() will override an existing apikey header (including user-supplied keys forwarded by the server proxy). To preserve the documented precedence of caller-provided keys, skip injecting apikey when the request already has one (or when it differs), and only inject the default when missing.
    const authHeaders = await getAuthHeaders()
    init = init || {}
    const headers = new Headers(init.headers || {})
    // set() not append(): the request may already carry Authorization/apikey
    // (the server-side proxy pre-authenticates its outbound request, which
    // flows through this override). append() would comma-join into
    // "Bearer <tok>, Bearer <tok>", which the backend rejects as a malformed JWT.
    for (const [key, value] of Object.entries(authHeaders)) {
      headers.set(key, value)
    }

Comment on lines 68 to 76
const authHeaders = await getAuthHeaders()
const headers = new Headers(options.headers || {})
// set() not append(): a request may already carry Authorization/apikey
// (e.g. the server-side proxy pre-authenticates its outbound request).
// append() would comma-join into "Bearer <tok>, Bearer <tok>", which the
// backend parses as a malformed JWT and rejects with 401.
for (const [key, value] of Object.entries(authHeaders)) {
headers.append(key, value)
headers.set(key, value)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants